<?php
class Hydrum_Controller_Dispatcher_Standard implements Zend_Controller_Dispatcher_Interface
{
    /**
     * Default module name
     * @var string
     */
    const DEFAULT_MODULE = 'default';

    /**
     * Default action name
     *
     * @var string
     */
    const DEFAULT_ACTION = 'index';

    /**
     * Default controller name
     *
     * @var string
     */
    const DEFAULT_CONTROLLER = 'index';

    /**
     * List of controllers directories
     *
     * @var array
     */
    protected $directories;

    /**
     * Response object
     *
     * @var Zend_Controller_Response_Abstract
     */
    protected $response;

    /**
     * Dispatching parameters
     *
     * @var array
     */
    protected $params = array();

    /**
     * Formats a string into a controller name.  This is used to take a raw
     * controller name, such as one that would be packaged inside a request
     * object, and reformat it to a proper class name that a class extending
     * Zend_Controller_Action would use.
     *
     * @param string $unformatted
     * @return string
     */
    public function formatControllerName($unformatted)
    {
        return $this->formatName($unformatted);
    }

    /**
     * Formats a string into a module name.  This is used to take a raw
     * module name, such as one that would be packaged inside a request
     * object, and reformat it to a proper directory/class name that a class extending
     * Zend_Controller_Action would use.
     *
     * @param string $unformatted
     * @return string
     */
    public function formatModuleName($unformatted)
    {
        return $this->formatName($unformatted);
    }

    /**
     * Formats a string into an action name.  This is used to take a raw
     * action name, such as one that would be packaged inside a request
     * object, and reformat into a proper method name that would be found
     * inside a class extending Zend_Controller_Action.
     *
     * @param string $unformatted
     * @return string
     */
    public function formatActionName($unformatted)
    {
        $formatted = $this->formatName($unformatted, true);

        return strtolower(substr($formatted, 0, 1)) . substr($formatted, 1) . 'Action';
    }

    /**
     * Add or modify a parameter with which to instantiate an Action Controller
     *
     * @param string $name
     * @param mixed $value
     * @return Zend_Controller_Dispatcher_Interface
     */
    public function setParam($name, $value)
    {
        $this->params[$name] = $value;

        return $this;
    }

    /**
     * Set an array of a parameters to pass to the Action Controller constructor
     *
     * @param array $params
     * @return Zend_Controller_Dispatcher_Interface
     */
    public function setParams(array $params)
    {
        $this->params = array_merge($this->params, $params);

        return $this;
    }

    /**
     * Retrieve a single parameter from the controller parameter stack
     *
     * @param string $name
     * @return mixed
     */
    public function getParam($name)
    {
        $param = null;

        if (isset($this->params[$name])) {
            $param = $this->params[$name];
        }

        return $param;
    }

    /**
     * Retrieve the parameters to pass to the Action Controller constructor
     *
     * @return array
     */
    public function getParams()
    {
        return $this->params;
    }

    /**
     * Clear the controller parameter stack
     *
     * By default, clears all parameters. If a parameter name is given, clears
     * only that parameter; if an array of parameter names is provided, clears
     * each.
     *
     * @param null|string|array single key or array of keys for params to clear
     * @return Zend_Controller_Dispatcher_Interface
     */
    public function clearParams($name = null)
    {
        if ($name == null) {
            $this->params = array();
        } elseif (is_string($name) && isset($this->params[$name])) {
            unset($this->params[$name]);
        } elseif (is_array($name)) {
            foreach ($name as $key) {
                if (is_string($key) && isset($this->params[$key])) {
                    unset($this->params[$key]);
                }
            }
        }

        return $this;
    }

    /**
     * Set the response object to use, if any
     *
     * @param Zend_Controller_Response_Abstract|null $response
     * @return void
     */
    public function setResponse(Zend_Controller_Response_Abstract $response = null)
    {
        $this->response = $response;

        return $this;
    }

    /**
     * Retrieve the response object, if any
     *
     * @return Zend_Controller_Response_Abstract|null
     */
    public function getResponse()
    {
        return $this->response;
    }

    /**
     * Add a controller directory to the controller directory stack
     *
     * @param string $path
     * @param string $args
     * @return Zend_Controller_Dispatcher_Interface
     */
    public function addControllerDirectory($path, $module = null)
    {
        if ($module == null) {
            $module = $this->getDefaultModule();
        }

        $module = strtolower($module);

        $path = rtrim((string) $path, '/\\');

        if (!isset($this->directories[$module])) {
            $this->directories[$module] = array();
        }

        array_unshift($this->directories[$module], $path);

        return $this;
    }

    /**
     * Set the directory where controller files are stored
     *
     * Specify a string or an array; if an array is specified, all paths will be
     * added.
     *
     * @param string|array $dir
     * @return Zend_Controller_Dispatcher_Interface
     */
    public function setControllerDirectory($path)
    {
        $this->directories = array();

        $this->addControllerDirectory($path);

        return $this;
    }

    /**
     * Return the currently set directory(ies) for controller file lookup
     *
     * @return array
     */
    public function getControllerDirectory($moduleName = null, $controllerName = null)
    {
        $result = null;

        $directories = $this->getControllerDirectories($moduleName);

        if ($directories) {
            foreach ($directories as $directory) {
                $file = $directory;

                if ($controllerName !== null) {
                    $file .= DS . $controllerName . '.php';
                }

                if (file_exists($file)) {
                    $result = $directory;
                    break;
                }
            }
        }

        return $result;
    }

    /**
     * Return the currently set directory(ies) for controller file lookup
     *
     * @return array
     */
    public function getControllerDirectories($module = null)
    {
        $directory = null;

        if ($module === null) {
            $directory = $this->directories;
        } elseif (isset($this->directories[$module])) {
            $directory = $this->directories[$module];
        }

        return $directory;
    }

    /**
     * Formats a string from a URI into a PHP-friendly name.
     *
     * By default, replaces words separated by the word separator character(s)
     * with camelCaps. If $isAction is false, it also preserves replaces words
     * separated by the path separation character with an underscore, making
     * the following word Title cased. All non-alphanumeric characters are
     * removed.
     *
     * @param string $unformatted
     * @param boolean $isAction Defaults to false
     * @return string
     */
    protected function formatName($unformatted, $isAction = false)
    {
        // preserve directories
        if (!$isAction) {
            $segments = explode('_', $unformatted);
        } else {
            $segments = (array) $unformatted;
        }

        foreach ($segments as $key => $segment) {
            $segment = str_replace('-', ' ', strtolower($segment));
            $segment = preg_replace('/[^a-z0-9 ]/', '', $segment);
            $segments[$key] = str_replace(' ', '', ucwords($segment));
        }

        return implode('_', $segments);
    }

    /**
     * Format action class name
     *
     * @param string $moduleName Name of the current module
     * @param string $className Name of the action class
     * @return string Formatted class name
     */
    public function formatClassName($moduleName, $controllerName)
    {
        $moduleName = $this->formatName($moduleName);
        $controllerName = $this->formatName($controllerName);

        return 'Module_' . $moduleName . '_Controller_' . $controllerName;
    }

    /**
     * Get controller class name
     *
     * Try request first; if not found, try pulling from request parameter;
     * if still not found, fallback to default
     *
     * @param Zend_Controller_Request_Abstract $request
     * @return string|null Returns class name on success
     */
    public function getControllerClass(Zend_Controller_Request_Abstract $request)
    {
        $className = null;

        $controllerName = $this->getControllerName($request);

        $moduleName = $request->getModuleName();

        $className = $this->formatClassName($moduleName, $controllerName);

        return $className;
    }

    /**
     * Get path to file which contains controller class by request
     *
     * @param Zend_Controller_Request_Abstract $request
     * @return string
     */
    public function getControllerFile(Zend_Controller_Request_Abstract $request)
    {
        $result = null;

        $controllerName = $this->getControllerName($request);

        $moduleName = $request->getModuleName();

        $controllerName = $this->formatControllerName($controllerName);

        $directory = $this->getControllerDirectory($moduleName, $controllerName);

        if ($directory) {
            $result = $directory . DS . $controllerName . '.php';
        }

        return $result;
    }

    /**
     * Get controller name by request
     *
     * @param Zend_Controller_Request_Abstract $request
     * @return string
     */
    protected function getControllerName(Zend_Controller_Request_Abstract $request)
    {
        $controllerName = $request->getControllerName();

        if (empty($controllerName)) {
            $controllerName = $this->getDefaultControllerName();
            $request->setControllerName($controllerName);
        }

        return $controllerName;
    }

    /**
     * Returns TRUE if an action can be dispatched, or FALSE otherwise.
     *
     * @param  Zend_Controller_Request_Abstract $request
     * @return boolean
     */
    public function isDispatchable(Zend_Controller_Request_Abstract $request)
    {
        $isDispatchable = false;

        $module = $request->getModuleName();

        if ($this->isValidModule($module)) {
            $className = $this->getControllerClass($request);

            $isDispatchable = class_exists($className, false) || $this->getControllerFile($request);
        }

        return $isDispatchable;
    }

    /**
     * Dispatches a request object to a controller/action.  If the action
     * requests a forward to another action, a new request will be returned.
     *
     * @param  Zend_Controller_Request_Abstract $request
     * @param  Zend_Controller_Response_Abstract $response
     * @return void
     */
    public function dispatch(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response)
    {
        $this->setResponse($response);

        /**
         * Get controller class
         */
        if ($this->isDispatchable($request)) {
            $className = $this->getControllerClass($request);
        } else {
            $controller = $request->getControllerName();

            if (!empty($controller)) {
                throw new Zend_Controller_Dispatcher_Exception('Invalid controller specified (' . $request->getControllerName() . ')');
            }
        }

        if (!class_exists($className, false)) {
            require_once $this->getControllerFile($request);
        }

        /**
         * Instantiate controller with request, response, and invocation
         * arguments; throw exception if it's not an action controller
         */
        $controller = new $className($request, $this->getResponse(), $this->getParams());

        if (!($controller instanceof Zend_Controller_Action_Interface) && !($controller instanceof Zend_Controller_Action)) {

            throw new Zend_Controller_Dispatcher_Exception('Controller "' . $className . '" is not an instance of Zend_Controller_Action_Interface');
        }

        /**
         * Retrieve the action name
         */
        $action = $this->getActionMethod($request);

        /**
         * Dispatch the method call
         */
        $request->setDispatched(true);

        // by default, buffer output
        $disableOb = $this->getParam('disableOutputBuffering');
        $obLevel = ob_get_level();
        if (empty($disableOb)) {
            ob_start();
        }

        try {
            $controller->dispatch($action);
        } catch (Exception $e) {
            // Clean output buffer on error
            $curObLevel = ob_get_level();
            if ($curObLevel > $obLevel) {
                do {
                    ob_get_clean();
                    $curObLevel = ob_get_level();
                } while ($curObLevel > $obLevel);
            }
            throw $e;
        }

        if (empty($disableOb)) {
            $content = ob_get_clean();
            $response->appendBody($content);
        }

        // Destroy the page controller instance and reflection objects
        $controller = null;
    }

    /**
     * Whether or not a given module is valid
     *
     * @param string $module
     * @return boolean
     */
    public function isValidModule($module)
    {
        $directories = $this->getControllerDirectory($module);

        return (bool) count($directories);
    }

    /**
     * Determine the action name
     *
     * First attempt to retrieve from request; then from request params
     * using action key; default to default action
     *
     * Returns formatted action name
     *
     * @param Zend_Controller_Request_Abstract $request
     * @return string
     */
    public function getActionMethod(Zend_Controller_Request_Abstract $request)
    {
        $action = $request->getActionName();

        if (empty($action)) {
            $action = $this->getDefaultAction();
            $request->setActionName($action);
        }

        return $this->formatActionName($action);
    }

    /**
     * Retrieve the default controller name
     *
     * @return string
     */
    public function getDefaultControllerName()
    {
        return self::DEFAULT_CONTROLLER;
    }

    /**
     * Retrieve the default action
     *
     * @return string
     */
    public function getDefaultAction()
    {
        return self::DEFAULT_ACTION;
    }

    /**
     * Retrieve the default module name
     *
     * @return string
     */
    public function getDefaultModule()
    {
        return self::DEFAULT_MODULE;
    }
}
